﻿using System;
using System.Linq.Expressions;
using Moq;

namespace IslandUnit
{
    /// <summary>
    /// Contains possible setups to mocks created to the dependency.
    /// </summary>
    /// <typeparam name="TDependency">The type of the dependency.</typeparam>
    public class MockSetup<TDependency> : IHideObjectMembers where TDependency : class
    {
        /// <summary>
        /// Gets or sets the <see cref="Dependency&lt;TDependency&gt;"/> of your dependency.
        /// </summary>
        /// <value>
        /// The <see cref="Dependency&lt;TDependency&gt;"/> of your dependency.
        /// </value>
        internal Dependency<TDependency> Dependency { get; set; }

        /// <summary>
        /// Gets or sets the target method of the mock.
        /// </summary>
        /// <value>
        /// The target method of the mock.
        /// </value>
        internal object Method { get; set; }
        
        /// <summary>
        /// Gets or sets the kind of the method.
        /// </summary>
        /// <value>
        /// The kind of the method.
        /// </value>
        internal MethodKind MethodKind { get; set; }

        /// <summary>
        /// Gets or sets the number of calls the method of
        /// this mock can be called.
        /// </summary>
        /// <value>
        /// The number of calls.
        /// </value>
        internal Times Times { get; set; }
        
        /// <summary>
        /// Initializes a new instance of the <see cref="MockSetup&lt;TDependency&gt;"/> class.
        /// </summary>
        /// <param name="dependency">The <see cref="Dependency&lt;TDependency&gt;"/> of your dependency.</param>
        /// <param name="method">The target function of the mock.</param>
        public MockSetup(Dependency<TDependency> dependency, Expression<Func<TDependency, object>> method)
        {
            this.Dependency = dependency;
            this.Method = method;
            this.MethodKind = MethodKind.Function;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MockSetup&lt;TDependency&gt;"/> class.
        /// </summary>
        /// <param name="dependency">The <see cref="Dependency&lt;TDependency&gt;"/> of your dependency.</param>
        /// <param name="method">The target procedure of the mock.</param>
        public MockSetup(Dependency<TDependency> dependency, Expression<Action<TDependency>> method)
        {
            this.Dependency = dependency;
            this.Method = method;
            this.MethodKind = MethodKind.Action;
        }

        /// <summary>
        /// Setup the mock to expect the method of the mock never be called.
        /// </summary>
        /// <returns>An <see cref="MockAction&lt;TDependency&gt;"/>, where you can define other mock behaviors.</returns>
        public MockAction<TDependency> CannotBeCalled()
        {
            this.Times = Times.Never();

            var mockAction = new MockAction<TDependency>(this.Dependency);

            return mockAction;
        }

        /// <summary>
        /// Setup the mock to expect the method of the mock to be called
        /// an defined number of times.
        /// </summary>
        /// <returns>An <see cref="MockAction&lt;TDependency&gt;"/>, where you can define other mock behaviors.</returns>
        public MockAction<TDependency> HaveToBeCalled(int times)
        {
            this.Times = Times.Exactly(times);

            var mockAction = new MockAction<TDependency>(this.Dependency);

            return mockAction;
        }
        
        /// <summary>
        /// Convenience method expect the method of the mock to be called once.
        /// </summary>
        /// <returns>An <see cref="MockAction&lt;TDependency&gt;"/>, where you can define other mock behaviors.</returns>
        public MockAction<TDependency> HaveToBeCalledOnce()
        {
            this.Times = Times.Once();

            var mockAction = new MockAction<TDependency>(this.Dependency);

            return mockAction;
        }

        /// <summary>
        /// Setup the mock to expect the method of the mock to be called
        /// a minimum number of times.
        /// </summary>
        /// <returns>An <see cref="MockAction&lt;TDependency&gt;"/>, where you can define other mock behaviors.</returns>
        public MockAction<TDependency> HaveToBeCalledAtLeast(int times)
        {
            this.Times = Times.AtLeast(times);

            var mockAction = new MockAction<TDependency>(this.Dependency);

            return mockAction;
        }

        /// <summary>
        /// Convenience method expect the method of the mock to be called a minimum once.
        /// </summary>
        /// <returns>An <see cref="MockAction&lt;TDependency&gt;"/>, where you can define other mock behaviors.</returns>
        public MockAction<TDependency> HaveToBeCalledAtLeastOnce()
        {
            this.Times = Times.AtLeastOnce();

            var mockAction = new MockAction<TDependency>(this.Dependency);

            return mockAction;
        }

        /// <summary>
        /// Setup the mock to expect the method of the mock can be called
        /// a maximum number of times.
        /// </summary>
        /// <returns>An <see cref="MockAction&lt;TDependency&gt;"/>, where you can define other mock behaviors.</returns>
        public MockAction<TDependency> CanBeCalledAtMost(int times)
        {
            this.Times = Times.AtMost(times);

            var mockAction = new MockAction<TDependency>(this.Dependency);

            return mockAction;
        }

        /// <summary>
        /// Convenience method expect the method of the mock can be called a maximum once.
        /// </summary>
        /// <returns>An <see cref="MockAction&lt;TDependency&gt;"/>, where you can define other mock behaviors.</returns>
        public MockAction<TDependency> CanBeCalledAtMostOnce()
        {
            this.Times = Times.AtMostOnce();

            var mockAction = new MockAction<TDependency>(this.Dependency);

            return mockAction;
        }

        /// <summary>
        /// Setup the mock to expect the method of the mock can be called
        /// in a range number of times.
        /// </summary>
        /// <returns>An <see cref="MockAction&lt;TDependency&gt;"/>, where you can define other mock behaviors.</returns>
        public MockAction<TDependency> CanBeCalledBetween(int minTimes, int maxTimes)
        {
            this.Times = Times.Between(minTimes, maxTimes, Range.Inclusive);

            var mockAction = new MockAction<TDependency>(this.Dependency);

            return mockAction;
        }
    }
}
